#include "AudioRecorder.h"
#include <sys/param.h>
#include <unistd.h>

AudioRecorder::AudioRecorder()
{
    fAudioBuffer = NULL;
    fAudioUnit = NULL;
    fOutputAudioFile = NULL;
}

AudioRecorder::~AudioRecorder()
{
    OSStatus err;
    err = AudioUnitUninitialize(fAudioUnit);
    if (err)
        printf("AudioUnitUninit failed\n");

    err = CloseComponent(fAudioUnit);
    if (err)
        printf("Close AudioUnit failed\n");
}

// Convenience function to dispose of our audio buffers
void AudioRecorder::DestroyAudioBufferList(AudioBufferList* list)
{
	UInt32 i;

	if(list)
    {
        for(i = 0; i < list->mNumberBuffers; i++)
        {
			if(list->mBuffers[i].mData)
                free(list->mBuffers[i].mData);
		}
		free(list);
	}
}

// Configure and Initialize our AudioUnits, Audio Files, and Audio Buffers
OSStatus AudioRecorder::Configure(const char* inParentDir, const char* inFilename,
        AudioStreamBasicDescription* inASBD)
{
	OSStatus err = noErr;

	err = ConfigureAU();
	if(err == noErr)
		err = ConfigureOutputFile(inParentDir, inFilename, inASBD);
	return err;
}

OSStatus AudioRecorder::ConfigureAU()
{
	Component component;
	ComponentDescription description;
	OSStatus err = noErr;
	UInt32 param;
	AURenderCallbackStruct callback;
    AudioDeviceID fInputDeviceID;
    AudioStreamBasicDescription fDeviceFormat;
    UInt32 fAudioSamples;

	description.componentType = kAudioUnitType_Output;
	description.componentSubType = kAudioUnitSubType_HALOutput;
	description.componentManufacturer = kAudioUnitManufacturer_Apple;
	description.componentFlags = 0;
	description.componentFlagsMask = 0;
	if(component = FindNextComponent(NULL, &description))
	{
		err = OpenAComponent(component, &fAudioUnit);
		if(err != noErr)
		{
            printf("Open output audio unit failed: %d\n", err);
			fAudioUnit = NULL;
			return err;
		}
	}

	// Configure the AudioOutputUnit
	// You must enable the Audio Unit (AUHAL) for input and output for the same  device.
	// When using AudioUnitSetProperty the 4th parameter in the method
	// refer to an AudioUnitElement.  When using an AudioOutputUnit
	// for input the element will be '1' and the output element will be '0'.

	// Enable input on the AUHAL
	param = 1;
	err = AudioUnitSetProperty(fAudioUnit, kAudioOutputUnitProperty_EnableIO,
            kAudioUnitScope_Input, 1, &param, sizeof(UInt32));
	if(err == noErr)
	{
		// Disable Output on the AUHAL
		param = 0;
		err = AudioUnitSetProperty(fAudioUnit, kAudioOutputUnitProperty_EnableIO,
                kAudioUnitScope_Output, 0, &param, sizeof(UInt32));
	}

	// Select the default input device
	param = sizeof(AudioDeviceID);

    // If needed, lower privilege here
	err = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultInputDevice, &param, &fInputDeviceID);
    // and restore privilege here

    if(err != noErr)
	{
		printf("failed to get default input device\n");
		return err;
	}
    
	// Set the current device to the default input unit.
	err = AudioUnitSetProperty(fAudioUnit, kAudioOutputUnitProperty_CurrentDevice,
            kAudioUnitScope_Global, 0, &fInputDeviceID, sizeof(AudioDeviceID));
	if(err != noErr)
	{
		printf("failed to set AU input device\n");
		return err;
	}

	// Setup render callback
	// This will be called when the AUHAL has input data
	callback.inputProc = AudioInputProc; // defined as static in the header file
	callback.inputProcRefCon = this;
	err = AudioUnitSetProperty(fAudioUnit, kAudioOutputUnitProperty_SetInputCallback,
            kAudioUnitScope_Global, 0, &callback, sizeof(AURenderCallbackStruct));

	// get hardware device format
	param = sizeof(AudioStreamBasicDescription);
	err = AudioUnitGetProperty(fAudioUnit, kAudioUnitProperty_StreamFormat,
            kAudioUnitScope_Input, 1, &fDeviceFormat, &param);
	if(err != noErr)
	{
		printf("failed to get input device inASBD\n");
		return err;
	}

	// Twiddle the format to our liking
	fOutputFormat.mChannelsPerFrame = 1;
	fOutputFormat.mSampleRate = fDeviceFormat.mSampleRate;
	fOutputFormat.mFormatID = kAudioFormatLinearPCM;
	fOutputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved;
	fOutputFormat.mBitsPerChannel = sizeof(SInt16) * 8;
	fOutputFormat.mBytesPerFrame = fOutputFormat.mChannelsPerFrame * fOutputFormat.mBitsPerChannel / 8;
	fOutputFormat.mFramesPerPacket = 1;
	fOutputFormat.mBytesPerPacket = fOutputFormat.mBytesPerFrame;

	// Set the AudioOutputUnit output data format
	err = AudioUnitSetProperty(fAudioUnit, kAudioUnitProperty_StreamFormat,
            kAudioUnitScope_Output, 1, &fOutputFormat, sizeof(AudioStreamBasicDescription));
	if(err != noErr)
	{
		printf("failed to set input device inASBD\n");
		return err;
	}

	// Get the number of frames in the IO buffer(s)
	param = sizeof(UInt32);
	err = AudioUnitGetProperty(fAudioUnit, kAudioDevicePropertyBufferFrameSize,
            kAudioUnitScope_Global, 0, &fAudioSamples, &param);
	if(err != noErr)
	{
		printf("failed to get audio sample size\n");
		return err;
	}

	// Initialize the AU
	err = AudioUnitInitialize(fAudioUnit);
	if(err != noErr)
	{
		printf("failed to initialize AU\n");
		return err;
	}

	// Allocate our audio buffers
	fAudioBuffer = AllocateAudioBufferList(fOutputFormat.mChannelsPerFrame, fAudioSamples * fOutputFormat.mBytesPerFrame);
	if(fAudioBuffer == NULL)
	{
		printf("failed to allocate buffers\n");
		return err;
	}

	return noErr;
}

OSStatus AudioRecorder::ConfigureInputDevice(AudioDeviceID* inDeviceID)
{
    OSStatus err;

    err = AudioUnitUninitialize(fAudioUnit);
	if(err != noErr)
	{
		printf("failed to uninit AU input device\n");
		return err;
	}
    
    err = AudioUnitSetProperty(fAudioUnit, kAudioOutputUnitProperty_CurrentDevice,
            kAudioUnitScope_Global, 0, inDeviceID, sizeof(AudioDeviceID));
	if(err != noErr)
	{
		printf("failed to set new AU input device\n");
		return err;
	}

    err = AudioUnitInitialize(fAudioUnit);
	if(err != noErr)
	{
		printf("failed to reinit AU input device\n");
		return err;
	}
}

// Convenience function to allocate our audio buffers
AudioBufferList *AudioRecorder::AllocateAudioBufferList(UInt32 numChannels, UInt32 size)
{
	AudioBufferList* list;
	UInt32 i;

	list = (AudioBufferList*)calloc(1, sizeof(AudioBufferList) + numChannels * sizeof(AudioBuffer));
	if(list == NULL)
        return NULL;

	list->mNumberBuffers = numChannels;
	for(i = 0; i < numChannels; ++i)
    {
		list->mBuffers[i].mNumberChannels = 1;
		list->mBuffers[i].mDataByteSize = size;
		list->mBuffers[i].mData = malloc(size);
		if(list->mBuffers[i].mData == NULL)
        {
			DestroyAudioBufferList(list);
			return NULL;
		}
	}
	return list;
}

OSStatus AudioRecorder::AudioInputProc(void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags,
        const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber,
        UInt32 inNumberFrames, AudioBufferList* ioData)
{
	AudioRecorder *ar = (AudioRecorder*) inRefCon;
	OSStatus err = noErr;

	// Render into audio buffer
	err = AudioUnitRender(ar->fAudioUnit, ioActionFlags, inTimeStamp, inBusNumber,
            inNumberFrames, ar->fAudioBuffer);
	if(err)
		fprintf(stderr, "AudioUnitRender() failed with error %i\n", err);

	// Write to file, ExtAudioFile auto-magicly handles conversion/encoding
	// NOTE: Async writes may not be flushed to disk until a the file
	// reference is disposed using ExtAudioFileDispose
	err = ExtAudioFileWriteAsync(ar->fOutputAudioFile, inNumberFrames, ar->fAudioBuffer);
	if(err != noErr)
	{
		char formatID[5];
		*(UInt32 *)formatID = CFSwapInt32HostToBig(err);
		formatID[4] = '\0';
		fprintf(stderr, "ExtAudioFileWrite FAILED! %d '%-4.4s'\n",err, formatID);
		return err;
	}

	return err;
}

OSStatus AudioRecorder::ConfigureOutputFile(const char* inParentDir,
        const char* inFileName, AudioStreamBasicDescription* inASBD)
{
	OSStatus err = noErr;
    FSRef mParentDir;
    CFStringRef fileName;

    err = FSPathMakeRef((UInt8*) inParentDir, &mParentDir, NULL);
    if (err)
    {
        printf("Make Ref failed with err: %d\n", err);
        return err;
    }

    fileName = CFStringCreateWithFileSystemRepresentation(kCFAllocatorDefault, inFileName);
    if (!fileName)
    {
        printf("Create CFString from filename failed\n");
        return paramErr;
    }

	// Create new AIFF file
	err = ExtAudioFileCreateNew(&mParentDir, fileName, kAudioFileAIFFType,
            inASBD, NULL, &fOutputAudioFile);
	if(err != noErr)
	{
		char formatID[5];
		*(UInt32 *)formatID = CFSwapInt32HostToBig(err);
		formatID[4] = '\0';
		fprintf(stderr, "ExtAudioFileCreateNew FAILED! %d '%-4.4s'\n",err, formatID);
		return err;
	}

    CFRelease(fileName);
    
	// Inform the file what format the data is we're going to give it, should be pcm
	// You must set this in order to encode or decode a non-PCM file data format.
	err = ExtAudioFileSetProperty(fOutputAudioFile, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), &fOutputFormat);
	if(err != noErr)
	{
		char formatID[5];
		*(UInt32 *)formatID = CFSwapInt32HostToBig(err);
		formatID[4] = '\0';
		fprintf(stderr, "ExtAudioFileSetProperty FAILED! '%-4.4s'\n", formatID);
		return err;
	}

	// If we're recording from a mono source, setup a simple channel map to split to stereo
//	if (fDeviceFormat.mChannelsPerFrame == 1 && fOutputFormat.mChannelsPerFrame == 2)
//	{
//		// Get the underlying AudioConverterRef
//		UInt32 size = sizeof(AudioConverterRef);
//		err = ExtAudioFileGetProperty(fOutputAudioFile, kExtAudioFileProperty_AudioConverter, &size, &conv);
//		if (conv)
//		{
//			// This should be as large as the number of output channels,
//			// each element specifies which input channel's data is routed to that output channel
//			SInt32 channelMap[] = { 0, 0 };
//			err = AudioConverterSetProperty(conv, kAudioConverterChannelMap, 2*sizeof(SInt32), channelMap);
//		}
//	}

	// Initialize async writes thus preparing it for IO
	err = ExtAudioFileWriteAsync(fOutputAudioFile, 0, NULL);
	if(err != noErr)
	{
		char formatID[5];
		*(UInt32 *)formatID = CFSwapInt32HostToBig(err);
		formatID[4] = '\0';
		fprintf(stderr, "ExtAudioFileWriteAsync FAILED! '%-4.4s'\n", formatID);
		return err;
	}

	return err;
}

OSStatus AudioRecorder::Start()
{
	// Start pulling for audio data
	OSStatus err = AudioOutputUnitStart(fAudioUnit);
	if(err != noErr)
	{
		fprintf(stderr, "failed to start AU\n");
		return err;
	}

	fprintf(stderr, "Recording started...\n");
	return err;
}

OSStatus AudioRecorder::Stop()
{
	// Stop pulling audio data
	OSStatus err = AudioOutputUnitStop(fAudioUnit);
	if(err != noErr)
	{
		fprintf(stderr, "failed to stop AU\n");
		return err;
	}

    ExtAudioFileDispose(fOutputAudioFile);

	fprintf(stderr, "Recording stoped.\n");
	return err;
}
